require 'thread'

class Hash
  def to_s; "{#{inject('') {|s,(k,v)| s += "#{k.kind_of?(String) ? "'#{k}'" : k} => #{v.kind_of?(String) ? "'#{v}'" : v}, "}[0..-3]}}" end
end

class Array
  def to_s; "[#{inject('') {|s,v| s += "#{v.kind_of?(String) ? "'#{v}'" : v}, "}[0..-3]}]" end
end

class LimitedArray < Array
  attr_reader :max_size
  
  def initialize(size=0, obj=nil); @max_size = 200; super end
  def reduce; self.shift while self.length > @max_size; self end
  def set_size(max_size); @max_size = max_size; reduce end
  def push(line); super; reduce end
  def <<(line); push(line) end
end

class NilClass;
  def method_missing(*args); nil end
  def to_s; '' end
end

class RubyMudScript
  @@running = Array.new
  
  attr_reader :name, :file_name, :vars, :data, :thread_group
  attr_accessor :dying_procs, :hidden, :paused, :no_pause_all, :no_kill_all, :downstream_buffer, :upstream_buffer, :die_with, :watchfor
  
  def self.self
    return nil if !script = @@running.find {|s| s.thread_group == Thread.current.group}
    sleep(0.2) while script.paused
    script
  end
  def self.running; @@running.select {|s| !s.hidden} end
  def self.hidden; @@running.select {|s| s.hidden} end
  def self.exists?(scriptname)
    scriptname += '.rb' unless (scriptname =~ /\.rb$/i)
    $script_dir.each {|dir|
      File.exists?("#{dir}#{scriptname}") and File.dirname("#{dir}#{scriptname}") == dir.sub(/\/$/,'')
    }
  end
  def self.new_upstream(line)
    @@running.each {|s| s.upstream_buffer.push(line.chomp) if s.want_upstream}
  end
  def self.new_downstream(line)
    @@running.each {|script|
      script.downstream_buffer.push(line.chomp)
      next if script.watchfor.empty?
      script.watchfor.each_pair {|trigger, action|
        next if line !~ trigger
        new_thread = Thread.new {
          sleep(0.01) until RubyMudScript.self
          begin
            action.call
          rescue
            echo( "- RubyMud: Error in watchfor #{$!}")
          end
        }
        script.thread_group.add(new_thread)
      }
    }
  end
  
  def initialize(file_name, vars=[])
    init_base_vars(file_name)
    
    unless vars.empty?
      vars.each_index {|v| @vars[v+1] = vars[v]}
      @vars[0] = @vars[1..-1].join(' ')
      vars = nil
    end
    
    @data = ''
    begin
      File.open(file_name) {|f| f.readlines.each {|line| @data += line}}
    rescue
      echo("- RubyMud: Error reading script (#{file_name}): #{$!}")
      return nil
    end
    
    @thread_group = ThreadGroup.new
    @@running.push(self)
    self
  end
  
  def init_base_vars(file_name)
    @name = /([a-zA-Z0-9_]+)\.rb/.match(file_name).captures.first
    @file_name = file_name
    @vars = Array.new
    @downstream_buffer = LimitedArray.new
    @upstream_buffer = LimitedArray.new
    @watchfor = Hash.new
    @dying_procs = Array.new
    @die_with = Array.new
    @paused = false
    @hidden = false
    @no_pause_all = false
    @no_kill_all = false
  end
  
  def kill
    Thread.new {
      begin
        die_with, dying_procs = @die_with.dup, @dying_procs.dup
        @die_with, @dying_procs = nil, nil
        @thread_group.list.dup.each {|t| t.kill rescue() unless t == Thread.current}
        @thread_group.add(Thread.current)
        die_with.each {|s| stop_script script_name}
        @paused = false
        dying_procs.each {|s|
          begin
            s.call
          rescue SyntaxError
            echo("- RubyMud: Syntax error in dying block: #{$!}")
          rescue SystemExit
            nil
          rescue
            echo("- RubyMud: Error in dying block: #{$!}")
          end
        }
        @downstream_buffer = @upstream_buffer = nil
        @@running.delete(self)
        echo("- RubyMud: #{@name} has exited.")
        GC.start
      rescue
        echo($!)
        echo($!.backtrace[0..2])
      end
    }
  end
  
  def pause
    echo("- RubyMud: #{@name} paused.")
    @paused = true
  end
  def unpause
    echo("- RubyMud: #{@name} unpaused.")
    @paused = false
  end
  def clear
    to_return = @downstream_buffer.dup
    @downstream_buffer.clear
    to_return
  end
  def to_s; @name end
  def gets
    sleep(0.05) while @downstream_buffer.empty?
    @downstream_buffer.shift
  end
  def upstream_gets
    sleep(0.05) while @upstream_buffer.empty?
    @upstream_buffer.shift
  end
  def gets?; @downstream_buffer.empty? ? nil : @downstream_buffer.shift end
  def upstream_gets?; @upstream_buffer.empty? ? nil : @upstream_buffer.shift end
end

class RubyMudExec < RubyMudScript
  attr_reader :cmd_data
  def initialize(cmd_data, flags=Hash.new)
    num = 1
    num+=1 while @@running.find {|script| script.name == "exec#{num}"}
    @name = "exec#{num}"
    @cmd_data = cmd_data
    @vars = Array.new
    @downstream_buffer = Array.new
    @upstream_buffer = Array.new
    @die_with = Array.new
    @dying_procs = Array.new
    @watchfor = Hash.new
    @hidden = false
    @paused = false
    @thread_group = ThreadGroup.new
    @no_pause_all = false
    @no_kill_all = false
    
    @thread_group = ThreadGroup.new
    @@running.push(self)
    self
  end
end

class StreamHook
  @@stream_hooks ||= Hash.new
  
  def self.add(name, action)
    unless action.class == Proc
      echo("- StreamHook (#{name}): action parameter is not a Proc.")
      return false
    end
    @@stream_hooks[self.name] ||= Hash.new
    self.hooks[name] = action
  end
  def self.run(client_string)
    for key in self.hooks.keys
      client_string = self.hooks[key].call(client_string) rescue self.handle_error(key)
      return nil if client_string.nil?
    end
    client_string
  end
  def self.handle_error(key)
    self.hooks.delete(key)
    echo("- #{self.name} (#{key}): #{$!}")
    echo($!.backtrace.first)
    ''
  end
  def self.hooks; @@stream_hooks.key?(self.name) ? @@stream_hooks[self.name] : Hash.new end
  def self.remove(name); self.hooks.delete(name) end
  def self.list; self.hooks.keys.dup end
end

class UpstreamHook < StreamHook; end
class DownstreamHook < StreamHook; end
class KeyHook < StreamHook
  @@mod_list = {'alt'  => TkEvent::Event::StateMask::ALT_MASK,
                'ctrl' => TkEvent::Event::StateMask::ControlMask,
                'shift' => TkEvent::Event::StateMask::ShiftMask}
                
  def self.run(e)
    self.modifiers(e)
    process_key = true
    for key in self.hooks.keys
      process_key = self.hooks[key].call(e, self.modifiers(e)) rescue self.handle_error(key)
      break unless process_key
    end
    process_key
  end
  
  def self.modifiers(e); @@mod_list.inject([]) {|s,(k,v)| s << k if e.state & v != 0; s} end
end

class StringProc
  def initialize(s); @string = s end
  def call(*args); eval(@string) end
  def _dump(depth = nil); @string end
  def self._load(s); StringProc.new(s) end
  def inspect; "StringProc.new(#{@string.inspect})" end
end

def load_dat_file(filename, empty = nil)
  data = nil
  File.open("#{filename}.dat", 'rb') {|file| data = Marshal.load(file.read)}
  GC.start
  data.nil? ? empty : data
rescue
  echo("- RubyMud: File does not exist: #{"#{filename}.dat"}")
  empty
end

def save_dat_file(filename, data, backup = false)
  if File.exists?("#{filename}.dat") && backup
    echo("- RubyMud: Backing up file: #{filename}.dat as #{filename}.bak")
    File.open("#{filename}.dat", 'rb') {|infile|
      File.open("#{filename}.bak", 'wb') {|outfile| outfile.write(infile.read)}
    }
  end
  File.open("#{filename}.dat", 'wb') {|file| file.write(Marshal.dump(data))}
  echo("- RubyMud: Save successful.")
rescue
  echo("- RubyMud: Error in file save: #{$!}")
ensure
  GC.start
end

def log(filename = nil)
  filename = Time.now.to_i.to_s if filename.nil?
  text=$app.show_box.get(1.0,'end')
  File.open("#{filename}.log", "wb") {|o| o.write(text)}
end

def read_script(filename)
  data = ''
  begin
    File.open(filename) {|f| f.readlines.each {|line| data += line}}
  rescue
    echo("- RubyMud: Error reading script (#{filename}): #{$!}")
    return nil
  end
  data
end
def script_proc(filename); StringProc.new(read_script("scripts/#{filename}.rb")) end

def start_proc(proc, args=nil)
  Thread.new {
    Thread.current.priority = 1
    begin
      proc.call(args)
    rescue
      start_script_error($!, "Error")
    end
  }
  true
end

def start_exec_script(cmd_data, flags=Hash.new)
  unless new_script = RubyMudExec.new(cmd_data, flags)
    echo("- RubyMud: error starting exec script: #{$!}")
    return false
  end
  new_thread = Thread.new {
    100.times { break if RubyMudScript.self == new_script; sleep "0.01".to_f }
    if script = RubyMudScript.self
      Thread.current.priority = 1
      echo("- RubyMud: #{script.name} active.")
      begin
        eval(cmd_data, nil, script.name.to_s)
        RubyMudScript.self.kill
      rescue SystemExit
        RubyMudScript.self.kill
      rescue SyntaxError
        start_script_error($!, "SyntaxError")
      rescue ScriptError
        start_script_error($!, "ScriptError")
      rescue NoMemoryError
        start_script_error($!, "NoMemoryError")
      rescue LoadError
        start_script_error($!, "LoadError")
      rescue ThreadError
        start_script_error($!, "ThreadError")
      rescue SystemStackError
        start_script_error($!, "SystemStackError")
      rescue Exception
        start_script_error($!, "Exception")
      rescue
        start_script_error($!, "Error")
      end
    else
      echo('start_exec_script screwed up...')
    end
  }
  new_script.thread_group.add(new_thread)
  true
end

def add_script_dir(dir); $script_dir << dir unless $script_dir.include?(dir) end

def start_script(script_name,vars=[],flags=Hash.new)
  found_dir, file_name = '', nil
  $script_dir.each {|dir|
    if File.exists?("#{dir}#{script_name}.rb")
      file_name = "#{script_name}.rb"
      found_dir = dir
      break
    end
  }
  unless file_name
    echo("- RubyMud: could not find script '#{script_name}' in directories #{$script_dir.join(", ")}.")
    return false
  end
  if (RubyMudScript.running + RubyMudScript.hidden).find {|s| s.name == file_name.sub(/\.rb$/, '')}
    echo("- RubyMud: #{script_name} is already running.")
    return false
  end
  begin
    vars = vars.kind_of?(Array) ? vars : [vars]
    new_script = RubyMudScript.new("#{found_dir}#{file_name}", vars)
  rescue
    echo("- RubyMud: error starting script (#{script_name}): #{$!}")
    return false
  end
  unless new_script
    echo("- RubyMud: failed to start script (#{script_name})")
    return false
  end
  new_thread = Thread.new {
    100.times {break if RubyMudScript.self == new_script; sleep(0.01)}
    if script = RubyMudScript.self
      Thread.current.priority = 1
      echo("- RubyMud: #{script.name} active.")
      begin
        if script = RubyMudScript.self
          eval(script.data, nil, script.name) 
        end
        RubyMudScript.self.kill
      rescue SystemExit
        RubyMudScript.self.kill
      rescue SyntaxError
        start_script_error($!, "SyntaxError")
      rescue ScriptError
        start_script_error($!, "ScriptError")
      rescue NoMemoryError
        start_script_error($!, "NoMemoryError")
      rescue LoadError
        start_script_error($!, "LoadError")
      rescue ThreadError
        start_script_error($!, "ThreadError")
      rescue SystemStackError
        start_script_error($!, "SystemStackError")
      rescue Exception
        start_script_error($!, "Exception")
      rescue
        start_script_error($!, "Error")
      end
    else
      echo('start_script screwed up...')
    end
  }
  new_script.thread_group.add(new_thread)
  true
end

def start_script_error(e, type)
  echo("--- #{type}: #{e}")
  echo(e.backtrace.first)
  puts("--- #{type}: #{e}")
  puts(e.backtrace)
  RubyMudScript.self.kill
end

def start_scripts(*script_names)
  script_names.flatten.each { |script_name|
    start_script(script_name)
    sleep "0.02".to_f
  }
end

def stop_script(*stop_list)
  numkilled = 0
  stop_list.each {|stop_script| 
    condemned = (RubyMudScript.running + RubyMudScript.hidden).find {|s| s.name =~ /^#{stop_script}/i }
    if condemned.nil?
      echo("- RubyMud: '#{script_name}' tried to stop '#{stop_script}', but it isn't running!")
    else
      exit if condemned.name =~ /^#{RubyMudScript.self.name}$/i
      condemned.kill
      echo("- RubyMud: '#{condemned}' has been stopped by #{script_name}.")
      numkilled += 1
    end
  }
  numkilled == 0 ? false : numkilled
end

def hide_script(*args)
  args.flatten.each {|name| RubyMudScript.running.find {|s| s.name == name}.hidden = true}
end

def pause_script(*scripts)
  scripts.flatten!
  if scripts.empty?
    RubyMudScript.self.pause
    RubyMudScript.self
  else
    scripts.each {|name|
      found = (RubyMudScript.running + RubyMudScript.hidden).find {|s| s.name =~ /^#{name}/i }
      found.pause unless (found.paused || found.nil?)
    }
  end
end
def unpause_script(*scripts)
  scripts.flatten!
  scripts.each { |scr| 
    found = (RubyMudScript.running + RubyMudScript.hidden).find {|s| s.name =~ /^#{scr}/i }
    found.unpause if (found.paused and not found.nil?)
  }
end

def running?(*script_names)
  script_names.each {|name| return false unless (RubyMudScript.running + RubyMudScript.hidden).find {|s| s.name =~ /^#{name}$/i}}
  true
end

def upstream_get
  return nil if !script = self_script
  script.upstream_gets
end

def upstream_get?
  return nil if !script = self_script
  script.upstream_gets?
end

def die_with_me(*vals)
  return nil if !script = self_script
  script.die_with.push(vals).flatten!
  echo("- #{script.name}: The following script(s) will now die when I do: #{script.die_with.join(', ')}") unless script.die_with.empty?
end

def self_script
  if script = RubyMudScript.self then return script end
  echo("- RubyMud: Function #{caller(1).first.scan(/`(.+?)'/)[0][0]} cannot identify the calling script.")
  nil
end


def before_dying(&code)
  return nil if !script = self_script or code.nil?
  script.dying_procs.push(code)
  true
end

def clear_before_dying
  return nil if !script = self_script
  script.dying_procs.clear
  true
end

def abort!
  return nil if !script = self_script
  script.dying_procs.clear
  exit
end

def wait_until(time = 0)
  old_priority = Thread.current.priority
  Thread.current.priority = 0
  finish_time = Time.now.to_f + time
  until yield
    sleep(0.01)
    if time > 0 && Time.now.to_f > finish_time
      Thread.current.priority = old_priority
      return false
    end
  end
  true
end

def wait_while(time = 0)
  old_priority = Thread.current.priority
  Thread.current.priority = 0
  finish_time = Time.now.to_f + time
  while yield
    sleep(0.01)
    if time > 0 && Time.now.to_f > finish_time
      Thread.current.priority = old_priority
      return false
    end
  end
  Thread.current.priority = old_priority
end

def match_timeout(time, *strings)
  return nil if !script = self_script
  unless time.class == Float || time.class == Fixnum
    echo("- #{script.name}: matchtimeout error in time variable.")
    return false
  end
  strings.flatten!
  if strings.empty?
    echo("- #{script.name}: matchtimeout error in strings variable.")
    return false
  end
  regex_string = /#{strings.collect {|s| s.kind_of?(Regexp) ? s.source : s}.join('|')}/
  finish_time = Time.now.to_f + time
  loop do
    line = script.gets?
    return line if !line.nil? and line =~ regex_string
    return false if Time.now.to_f > finish_time
    sleep(0.01)
  end
end

def match_wait(*strings)
  return nil if !script = self_script
  strings.flatten!
  if strings.empty?
    echo("- #{script.name}: match_wait error in strings variable.")
    return false
  end
  regex_string = /#{strings.collect {|s| s.kind_of?(Regexp) ? s.source : s}.join('|')}/
  while line_in = script.gets
    return line_in if line_in =~ regex_string 
  end
end

def match_put(dict)
  regex_list = dict.keys.collect {|s| /#{s.kind_of?(Regexp) ? s.source : s}/}
  line = match_wait(regex_list)
  regex_list.each {|r| (put(dict[r])) if line =~ r}
  line
end

def put(*strings)
  strings.flatten!
  strings.each do |s|
    next if s.strip.empty?
    echo("[#{script_name}] #{s}")
    $app.send_text(s+"\n")
  end
end

def set_text(cmd)
  $app.set_type_box_text(cmd)
end

$key_binds ||= {}
def bind_key(mod, key, *actions); $key_binds[key] = [mod, actions.flatten] end
def unbind_key(key); $key_binds.delete(key) end

def echo(*messages); messages.each {|m| $app.show_message("#{m.to_s}\n")} end
def main_thread?; Thread.current == Thread.main end
def send_scripts(*info); info.flatten.each {|info| RubyMudScript.new_downstream(message)} end
def hide_me; RubyMudScript.self.hidden = !RubyMudScript.self.hidden end
def script_name; main_thread? ? "main_thread" : RubyMudScript.self end
def no_kill_all; RubyMudScript.self.no_kill_all = !RubyMudScript.self.no_kill_all end
def no_pause_all; RubyMudScript.self.no_pause_all = !RubyMudScript.self.no_pause_all end

$worlds ||= []
def del_world(world) $worlds.delete(world) end
def add_world(world)
  return if $worlds.include?(world)
  $worlds << world
  load_world(world)
end
def load_world(world)
  Dir[$world_dir+world+'/*.rb'].each {|f| load f}
  load $world_dir+world+'.rb'
end
def load_worlds; $worlds.each {|world| load_world(world)} end

$build_map = false
UpstreamHook.add('rm_script_handler', Proc.new {|cs|
  if cs.start_with?($rm_char)
    cs = cs[1..-1]
    cs = script_handler_addon(cs) if defined?(script_handler_addon)
    if cs =~ /^(?:k|kill) ?(.*)$/
      stop_script($1)
    elsif cs =~ /^(?:r|restart) (.+)$/
      if stop_script($1.split[0])
        start_proc(Proc.new {
          wait_while {running?($1)}
          start_script($1.split[0], $1.split[1..-1])
        })
      end
    elsif cs =~ /^(?:rl|reload)$/
      load_worlds
      echo "- RubyMud: Worlds reloaded."
    elsif cs =~ /^(?:e|exec) (.+)$/
      start_exec_script($1)
    elsif cs =~ /^list$/
      echo "- RubyMud: #{(RubyMudScript.running + RubyMudScript.hidden).collect {|s| s.name}.join(', ').to_s}"
    elsif cs =~ /^(.+)$/
      start_script($1.split[0], $1.split[1..-1])
    end
    nil
  else
    cs
  end
})

$echo_keys ||= false
KeyHook.add('rm_key_handler', Proc.new {|e, m|
  echo "KEY: #{e.keycode} - #{e.keysym} - #{m.to_s}" if $echo_keys
  if e.keysym == 'Up'
    $app.show_previous_cmd
    false
  elsif e.keysym == 'Down'
    $app.show_next_cmd
    false
  else
    $key_binds.inject(true) {|s,(k,v)|
      if m == v[0] && k == (k.class == String ? e.keysym : e.keycode)
        v[1].each {|cmd| put cmd}
        s = false
      end
      s
    }
  end
})
